﻿/*
    PerstSyncProvider
    Copyright (C) 2011 Mikolas Bingemer, MediFox GmbH

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
    MA 02110-1301  USA.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Reflection;
using Microsoft.Synchronization.ClientServices;
using Perst;
using Perst.Impl;

namespace MediFox.Synchronization.ClientServices.PerstSyncProvider
{

    /// <summary>
    /// This class maintains an offline cache of entities using Perst4.NET
    /// </summary>
    internal class StorageHandler : IDisposable
    {
        private readonly string STORAGE_FILENAME = "storage";
        private PerstOfflineSchema _schema;
        private string _cachePath;
        private long _pagePoolSize;
        private string _cipherKey;
        private Database _database;
        private bool _isDisposed;
        private Dictionary<Guid, IEnumerable<PerstOfflineEntity>> _pendingChanges = new Dictionary<Guid, IEnumerable<PerstOfflineEntity>>();
        private Dictionary<string, Dictionary<string, Query>> _cachedQueries = new Dictionary<string, Dictionary<string, Query>>();

        #region Properties

        public Database Database
        {
            get
            {
                ThrowIfDisposed();
                return _database;
            }
        }

        public bool IsDisposed
        {
            get
            {
                return _isDisposed;
            }
        }

        public bool IsEncrypted
        {
            get
            {
                return (_cipherKey != null);
            }

        }
        #endregion

        #region ctor

        public StorageHandler(PerstOfflineSchema schema, string cachePath)
            : this(schema, cachePath, StorageImpl.DEFAULT_PAGE_POOL_SIZE, null)
        {
        }

        public StorageHandler(PerstOfflineSchema schema, string cachePath, long pagePoolSize)
            : this(schema, cachePath, pagePoolSize, null)
        {
        }

        public StorageHandler(PerstOfflineSchema schema, string cachePath, string cipherKey)
            : this(schema, cachePath, StorageImpl.DEFAULT_PAGE_POOL_SIZE, cipherKey)
        {
        }

        public StorageHandler(PerstOfflineSchema schema, string cachePath, long pagePoolSize, string cipherKey)
        {
            Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), cachePath);

            _database = InitializeDatabase(schema, cachePath, pagePoolSize, cipherKey);

            _cachePath = cachePath;
            _pagePoolSize = pagePoolSize;
            _cipherKey = cipherKey;

            // TODO: validate existing schema (if found in database)
            //throw new ArgumentException("Specified schema does not match schema previously used for cache path");

            // TODO: store schema in database (if not yet present)
            //System.Diagnostics.Debug.Assert(_schema.IsPersistent());

            // keep local reference to schema
            _schema = schema;

            // create tables and indices
            CreateTablesAndIndicies();
        }

        #endregion

        #region IDisposable

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                if (disposing)
                {
                    _schema = null;
                    foreach (var queriesForType in _cachedQueries.Values)
                    {
                        queriesForType.Clear();
                    }
                    _cachedQueries.Clear();
                    _pendingChanges.Clear();
                    if (_database != null)
                    {
                        if (_database.Storage.IsOpened())
                        {
                            _database.Storage.Commit();
                            _database.Storage.Close();
                        }
                        _database.Storage.Listener = null;
                        _database = null;
                    }
                }
                _isDisposed = true;
            }
        }

        #endregion

        #region private methods

        private Database InitializeDatabase(PerstOfflineSchema schema, string cachePath, long pagePoolSize, string cipherKey)
        {
            // create Perst storage
            Storage storage = StorageFactory.Instance.CreateStorage();

            // register additional assemblies in order to resolve persistet types
            RegisterSchemaAssemblies(storage, schema);

            // create directory if necessary
            using (IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (!file.DirectoryExists(cachePath))
                {
                    file.CreateDirectory(cachePath);
                }
            }

            // prepare storage file name
            string storageFileName = Path.Combine(cachePath, STORAGE_FILENAME);

            if (!string.IsNullOrEmpty(cipherKey))
            {
                // open encrypted Perst storage
                storage.Open(storageFileName, pagePoolSize, cipherKey);
            }
            else
            {
                // open unencrypted Perst storage
                storage.Open(storageFileName, pagePoolSize);
            }

            // initialize Perst database wrapper
            Database database = new Database(storage);

            // create and assign storage listener
            storage.Listener = new StorageListener(database);

            return database;
        }

        private void RegisterSchemaAssemblies(Storage storage, PerstOfflineSchema schema)
        {
            // collect assemblies for schema types
            Dictionary<int, Assembly> assemblies = new Dictionary<int, Assembly>();
            foreach (Type entityType in schema.Collections)
            {
                Assembly entityAssembly = entityType.Assembly;
                int assemblyHashCode = entityAssembly.GetHashCode();
                if (!assemblies.ContainsKey(assemblyHashCode))
                {
                    assemblies.Add(assemblyHashCode, entityAssembly);
                }
            }

            foreach (Assembly assembly in assemblies.Values)
            {
                // register assembly in storage
                storage.RegisterAssembly(assembly);
            }
        }

        private bool CopyFile(IsolatedStorageFile iso, string sourceFileName, string targetFileName)
        {
            bool result = false;
            if (iso.FileExists(sourceFileName))
            {
                try
                {
                    using (var readStream = new IsolatedStorageFileStream(sourceFileName, FileMode.Open, FileAccess.Read, iso))
                    {
                        using (var writeStream = new IsolatedStorageFileStream(targetFileName, FileMode.Create, FileAccess.Write, iso))
                        {
                            using (var reader = new BinaryReader(readStream))
                            {
                                using (var writer = new BinaryWriter(writeStream))
                                {
                                    byte[] buffer = new byte[1024];
                                    int bytesRead;
                                    while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) > 0)
                                    {
                                        writer.Write(buffer, 0, bytesRead);
                                    }
                                    result = true;
                                }
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    System.Diagnostics.Debug.WriteLine("{0}: {1}", e.GetType().Name, e.Message);
                }
            }
            return result;
        }

        private void CreateTablesAndIndicies()
        {
            _database.BeginTransaction();
            try
            {
                // iterate all known types from schema
                foreach (Type entityType in _schema.Collections)
                {
                    CreateTableAndIndicies(entityType);
                }

                _database.CommitTransaction();
            }
            catch
            {
                _database.RollbackTransaction();
                throw;
            }
        }

        private void CreateTableAndIndicies(Type entityType)
        {
            // create table (if not yet present) - this is required before we can create an index
            _database.CreateTable(entityType);

            PropertyInfo[] entityKeyProperties = _schema.GetEntityKeyProperties(entityType);
            foreach (PropertyInfo keyProperty in entityKeyProperties)
            {
                // create index for this property (if not yet present)
                _database.CreateIndex(entityType, keyProperty.Name, (entityKeyProperties.Length == 1));
            }

            //    // TODO: no MultiFieldIndex support in Perst.Database?!
            //    string[] fieldNames = entityKeyProperties.Select(e => e.Name).ToArray<string>();
            //    MultiFieldIndex index = _database.Storage.CreateFieldIndex(entityType, fieldNames, true);
            // see http://forums.mcobject.com/index.php?showtopic=7806
        }

        private void DropTable(Type entityType)
        {
            Hashtable indices = _database.GetIndices(entityType);
            while (indices.Count > 0)
            {
                string key = (string)indices.Keys.ElementAt(0);
                //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("drop index {0}.{1}", entityType, key));
                _database.DropIndex(entityType, key);
            }

            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("drop table {0}", entityType));
            _database.DropTable(entityType);
        }

        private Dictionary<string, Query> GetQueriesForType(Type entityType)
        {
            Dictionary<string, Query> queriesForType;
            if (!_cachedQueries.TryGetValue(entityType.FullName, out queriesForType))
            {
                queriesForType = new Dictionary<string, Query>();
                _cachedQueries.Add(entityType.FullName, queriesForType);
            }
            return queriesForType;
        }

        private PerstOfflineEntity GetEntityByMetadataId(Type entityType, string metadataId, bool forUpdate = false)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("{0} {1}", entityType.Name, metadataId));

            Query query;

            // get cached queries for this type
            Dictionary<string, Query> queriesForType = GetQueriesForType(entityType);

            // look for a cached query
            if (!queriesForType.TryGetValue("MetadataId", out query))
            {
                // prepare query
                query = _database.CreateQuery(entityType, forUpdate);
                CodeGenerator code = query.GetCodeGenerator();
                code.Predicate(code.Eq(code.Invoke("MetadataId", new Code[0]), code.Parameter(1, typeof(string))));

                // cache query
                queriesForType.Add("MetadataId", query);
            }

            // assign parameter value
            query[1] = metadataId;

            // execute query
            PerstOfflineEntity entity = query.Execute(_database.GetRecords(entityType)).Cast<PerstOfflineEntity>().FirstOrDefault();

            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), (entity != null) ? string.Format("Found oid {0}", entity.Oid) : "Not found");
            return entity;
        }

        private IEnumerable<PerstOfflineEntity> GetDirtyEntities(Type entityType)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), entityType.Name);

            Query query;

            // get cached queries for this type
            Dictionary<string, Query> queriesForType = GetQueriesForType(entityType);

            // look for a cached query
            if (!queriesForType.TryGetValue("IsDirty", out query))
            {
                // prepare query
                query = _database.CreateQuery(entityType);
                CodeGenerator code = query.GetCodeGenerator();
                code.Predicate(code.Eq(code.Invoke("IsDirty", new Code[0]), code.Literal(true)));

                // cache query
                queriesForType.Add("IsDirty", query);
            }

            // execute query
            IEnumerable<PerstOfflineEntity> entities = query.Execute(_database.GetRecords(entityType)).Cast<PerstOfflineEntity>();

            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("Found {0} dirty {1} entities", entities.Count(), entityType.Name));
            return entities;
        }

        [Obsolete("not used")]
        private IEnumerable<PerstOfflineEntity> GetTombstonedEntities(Type entityType)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), entityType.Name);

            Query query;

            // get cached queries for this type
            Dictionary<string, Query> queriesForType = GetQueriesForType(entityType);

            // look for a cached query
            if (!queriesForType.TryGetValue("MetadataIsTombstone", out query))
            {
                // prepare query
                query = _database.CreateQuery(entityType);
                CodeGenerator code = query.GetCodeGenerator();
                code.Predicate(code.Eq(code.Invoke("MetadataIsTombstone", new Code[0]), code.Literal(true)));

                // cache query
                queriesForType.Add("MetadataIsTombstone", query);
            }

            // execute query
            IEnumerable<PerstOfflineEntity> entities = query.Execute(_database.GetRecords(entityType)).Cast<PerstOfflineEntity>();

            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("Found {0} tombstoned {1} entities", entities.Count(), entityType.Name));
            return entities;
        }

        private PerstOfflineEntity GetEntityByExample(PerstOfflineEntity incomingEntity, bool forUpdate = false)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            PerstOfflineEntity existingEntity = null;
            if (!string.IsNullOrEmpty(incomingEntity.ServiceMetadata.Id))
            {
                existingEntity = GetEntityByMetadataId(incomingEntity.GetType(), incomingEntity.ServiceMetadata.Id, forUpdate);
            }
            if (existingEntity == null)
            {
                existingEntity = GetEntityByPrimaryKey(incomingEntity, forUpdate);
            }
            return existingEntity;
        }

        private PerstOfflineEntity GetEntityByPrimaryKey(PerstOfflineEntity incomingEntity, bool forUpdate = false)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            PerstOfflineEntity entity = null;

            Type entityType = incomingEntity.GetType();

            Query query;

            // get cached queries for this type
            Dictionary<string, Query> queriesForType = GetQueriesForType(entityType);

            string queryKey = string.Format("Primary{0}", Convert.ToInt32(forUpdate));

            // look for a cached query
            if (!queriesForType.TryGetValue(queryKey, out query))
            {
                // create query
                query = _database.CreateQuery(entityType, forUpdate);

                // generate a primary key predicate for this type
                GeneratePrimaryKeyPredicate(query, entityType);

                // TODO: is it valid to cache "forUpdate" queries?
                //System.Diagnostics.Debug.Assert(!forUpdate);

                // cache query
                queriesForType.Add(queryKey, query);
            }

            // get primary key values of incomming entity
            object[] primaryKeyValues = _schema.GetPrimaryKeyValues(incomingEntity);
            for (int i = 0; i < primaryKeyValues.Length; i++)
            {
                // assign values to query parameters
                query[i + 1] = primaryKeyValues[i];
            }

            // execute query
            IEnumerable result = query.Execute(_database.GetRecords(entityType));

            // access result
            IEnumerable<PerstOfflineEntity> typedResult = result.Cast<PerstOfflineEntity>();
            entity = typedResult.FirstOrDefault();

            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), (entity != null) ? string.Format("Found oid {0}", entity.Oid) : "Not found");
            return entity;
        }

        private void GeneratePrimaryKeyPredicate(Query query, Type entityType)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), entityType.Name);

            // get a JSQL CodeGenerator
            CodeGenerator generator = query.GetCodeGenerator();
            Code primaryKeyPredicate = null;

            // retrieve key properties for this type
            PropertyInfo[] keyProperties = _schema.GetEntityKeyProperties(entityType);
            for (int i = 0; i < keyProperties.Length; i++)
            {
                PropertyInfo keyProperty = keyProperties[i];

                string propertyGetterName = keyProperty.GetGetMethod(true).Name;

                // generate JSQL code to compare property with query parameter
                Code propertyCode = generator.Invoke(propertyGetterName, new Code[0]);
                Code parameterCode = generator.Parameter(i + 1, keyProperty.PropertyType);
                Code comparisonCode = generator.Eq(propertyCode, parameterCode);

                if (primaryKeyPredicate == null)
                {
                    primaryKeyPredicate = comparisonCode;
                }
                else
                {
                    primaryKeyPredicate = generator.And(primaryKeyPredicate, comparisonCode);
                }

                // retrieve index for this property
                GenericIndex index = _database.GetIndex(entityType, keyProperty.Name);
                System.Diagnostics.Debug.Assert(index != null);

                // add index to query
                query.AddIndex(propertyGetterName, index);
            }

            // assign generated code as query predicate
            generator.Predicate(primaryKeyPredicate);
        }

        private void DeleteByIncomingEntity(PerstOfflineEntity incomingEntity)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            // Delete using the metadata id since tombstones do not have the primary keys filled in.
            // For deletes that are not the result of an upload/download response, we can use the primary keys.
            PerstOfflineEntity existingEntity = GetEntityByExample(incomingEntity);

            if (existingEntity != null)
            {
                DeleteEntity(existingEntity);
            }
            else
            {
                System.Diagnostics.Debug.Assert(false);
                //throw new DatabaseException("No entity of this type and with this primary key found in database", incomingEntity);
            }

        }

        private void DeleteEntity(PerstOfflineEntity existingEntity)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), existingEntity.GetType().Name);

            if (existingEntity.IsPersistent())
            {
                // remove IsDirty flag
                existingEntity.IsDirty = false;

                if (!_database.DeleteRecord(existingEntity))
                {
                    Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("Failed to delete entity {0} {1}", existingEntity.GetType().Name, existingEntity.Oid));
                    throw new DatabaseException("Failed to delete entity", existingEntity);
                }
            }
        }

        private void AddOrUpdateEntity(PerstOfflineEntity incomingEntity)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(incomingEntity.ServiceMetadata.Id));
            PerstOfflineEntity existingEntity = GetEntityByExample(incomingEntity, true);

            if (existingEntity == null)
            {
                // ensure that the IsDirty property is initialized for downloaded entities
                incomingEntity.IsDirty = false;
                _database.AddRecord(incomingEntity);
            }
            else
            {
                UpdateEntity(existingEntity, incomingEntity);
                // remove IsDirty flag from updated entities
                existingEntity.IsDirty = false;
            }
        }

        private void UpdateEntity(PerstOfflineEntity existingEntity, PerstOfflineEntity incomingEntity)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            PropertyInfo[] properties = _schema.GetEntityProperties(incomingEntity.GetType());
            foreach (PropertyInfo property in properties)
            {
                // read property value from incomming entity
                object incomingValue = property.GetValue(incomingEntity, new object[0]);

                // TODO: prevent unnecessary PropertyChanged events. maybe do it in PerstOfflineEntity instead?

                //// read property value from existing entity
                //object existingValue = property.GetValue(existingEntity, new object[0]);

                //// compare values
                //if (incomingValue != existingValue)   // TODO: use type specific overloaded operators to get expected behavior
                //{
                //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("{0}.{1} = {2}", incomingEntity.GetType().Name, property.Name, incomingValue));

                // update property on existing entity
                property.SetValue(existingEntity, incomingValue, new object[0]);
                //}
            }
        }

        private void ApplyIncomingEntity(PerstOfflineEntity incomingEntity)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod());

            // retrieve existing entity
            System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(incomingEntity.ServiceMetadata.Id));
            PerstOfflineEntity existingEntity = GetEntityByExample(incomingEntity, true);
            if (existingEntity != null)
            {
                // update local entity with server response
                UpdateEntity(existingEntity, incomingEntity);

                // assign sync metadata
                existingEntity.ServiceMetadata = incomingEntity.ServiceMetadata;

                // remove IsDirty flag
                existingEntity.IsDirty = false;
            }
            else
            {
                throw new DatabaseException("No entity of this type and with this primary key found in database", incomingEntity);
            }
        }

        private void ThrowIfDisposed()
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException("Cannot access disposed StorageHandler");
            }
        }

        #endregion

        #region public methods

        public IEnumerable<T> GetCollection<T>(bool forUpdate = false)
            where T : PerstOfflineEntity
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), typeof(T).Name);

            ThrowIfDisposed();

            // TODO: write collection wrapper

            return _database.GetRecords<T>(forUpdate);
        }

        public void AddEntity(PerstOfflineEntity incomingEntity)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            ThrowIfDisposed();

            _database.BeginTransaction();
            try
            {
                // TODO: rely on Unique indices instead
                PerstOfflineEntity existingEntity = GetEntityByPrimaryKey(incomingEntity);
                if (existingEntity == null)
                {
                    try
                    {
                        if (!_database.AddRecord(incomingEntity))
                        {
                            throw new DatabaseException("Failed to add entity", incomingEntity);
                        }
                    }
                    catch (StorageError e)
                    {
                        throw new DatabaseException("Failed to add entity", incomingEntity, e);
                    }
                }
                else
                {
                    throw new DatabaseException("An entity of this type and with the same primary key is allready in database", incomingEntity);
                }

                _database.CommitTransaction();
            }
            catch
            {
                _database.RollbackTransaction();
                throw;
            }

        }

        public void AddRange(IEnumerable<PerstOfflineEntity> incomingEntities)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            ThrowIfDisposed();

            _database.BeginTransaction();
            try
            {
                foreach (var incomingEntity in incomingEntities)
                {
                    // TODO: rely on Unique indices instead
                    PerstOfflineEntity existingEntity = GetEntityByPrimaryKey(incomingEntity);
                    if (existingEntity == null)
                    {
                        try
                        {
                            if (!_database.AddRecord(incomingEntity))
                            {
                                throw new DatabaseException("Failed to add entity", incomingEntity);
                            }
                        }
                        catch (StorageError e)
                        {
                            throw new DatabaseException("Failed to add entity", incomingEntity, e);
                        }
                    }
                    else
                    {
                        throw new DatabaseException("An entity of this type and with the same primary key is allready in database", incomingEntity);
                    }
                }

                _database.CommitTransaction();
            }
            catch
            {
                _database.RollbackTransaction();
                throw;
            }

        }

        public void TombstoneEntity(PerstOfflineEntity incomingEntity)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            ThrowIfDisposed();

            PerstOfflineEntity entity;

            _database.BeginTransaction();
            try
            {
                if (incomingEntity.IsPersistent())
                {
                    entity = incomingEntity;
                }
                else
                {
                    // TODO: when would this happen?
                    entity = GetEntityByExample(incomingEntity, true);
                    if (entity == null)
                    {
                        throw new DatabaseException("No entity of this type and with this primary key found in database", incomingEntity);
                    }
                }

                // mark as tombstoned
                entity.ServiceMetadata.IsTombstone = true;

                // delete property values
                PropertyInfo[] regularProperties = _schema.GetEntityNonKeyProperties(entity.GetType());
                foreach (PropertyInfo property in regularProperties)
                {
                    MethodInfo propertySetter = property.GetSetMethod();
                    propertySetter.Invoke(entity, new object[1]);
                }

                _database.CommitTransaction();
            }
            catch (Exception e)
            {
                _database.RollbackTransaction();
                throw new DatabaseException("Failed to tombstone entity", incomingEntity, e);
            }

        }

        public void DropEntity(PerstOfflineEntity incomingEntity)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            ThrowIfDisposed();

            PerstOfflineEntity entity;

            _database.BeginTransaction();
            try
            {
                if (incomingEntity.IsPersistent())
                {
                    entity = incomingEntity;
                }
                else
                {
                    entity = GetEntityByExample(incomingEntity, true);
                    if (entity == null)
                    {
                        throw new DatabaseException("No entity of this type and with this primary key found in database", incomingEntity);
                    }
                }

                _database.DeleteRecord(entity);

                _database.CommitTransaction();
            }
            catch (Exception e)
            {
                _database.RollbackTransaction();
                throw new DatabaseException("Failed to drop entity", incomingEntity, e);
            }

        }

        public void DropEntities(IEnumerable<PerstOfflineEntity> incomingEntities)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), incomingEntity.GetType().Name);

            ThrowIfDisposed();

            PerstOfflineEntity entity = null;

            _database.BeginTransaction();
            try
            {
                foreach (PerstOfflineEntity incomingEntity in incomingEntities)
                {
                    if (incomingEntity.IsPersistent())
                    {
                        entity = incomingEntity;
                    }
                    else
                    {
                        entity = GetEntityByExample(incomingEntity, true);
                        if (entity == null)
                        {
                            throw new DatabaseException("No entity of this type and with this primary key found in database", incomingEntity);
                        }
                    }

                    _database.DeleteRecord(entity);
                }

                _database.CommitTransaction();
            }
            catch (Exception e)
            {
                _database.RollbackTransaction();
                throw new DatabaseException("Failed to drop entity", entity, e);
            }

        }

        public void DropCollection<T>() where T : PerstOfflineEntity
        {
            ThrowIfDisposed();

            _database.BeginTransaction();
            try
            {
                DropTable(typeof(T));
                CreateTableAndIndicies(typeof(T));

                _database.CommitTransaction();
            }
            catch (Exception e)
            {
                _database.RollbackTransaction();
                throw new DatabaseException("Failed to drop collection", typeof(T), e);
            }
        }

        public bool HasDirty()
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod());

            ThrowIfDisposed();

            bool hasDirty = false;

            _database.BeginTransaction();
            try
            {
                // iterate over all known types
                foreach (Type entityType in _schema.Collections)
                {
                    // retrieve dirty entities of this type
                    IEnumerable<PerstOfflineEntity> dirtyEntities = GetDirtyEntities(entityType);

                    if (((IEnumerable)dirtyEntities).GetEnumerator().MoveNext())
                    {
                        hasDirty = true;
                        break;
                    }
                }
            }
            finally
            {
                _database.CommitTransaction();
            }

            return hasDirty;
        }

        /// <summary>
        /// Gets all entities that were created/modified/deleted locally after the last sync.
        /// </summary>
        /// <param name="state">A unique identifier for the changes that are uploaded</param>
        /// <returns>The set of incremental changes to send to the service.</returns>
        public IEnumerable<PerstOfflineEntity> GetChanges(Guid state)
        {
            Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), state.ToString());

            ThrowIfDisposed();

            // prepare list of dirty entities to sync
            List<PerstOfflineEntity> allDirtyEntities = new List<PerstOfflineEntity>();

            _database.BeginTransaction();
            try
            {
                // iterate over all known types
                foreach (Type entityType in _schema.Collections)
                {
                    // retrieve dirty entities of this type
                    IEnumerable<PerstOfflineEntity> dirtyEntities = GetDirtyEntities(entityType);

                    // add to list of dirty entities
                    allDirtyEntities.AddRange(dirtyEntities);
                }

                // keep track of pending changes
                _pendingChanges[state] = allDirtyEntities;
            }
            finally
            {
                _database.CommitTransaction();
            }

            return allDirtyEntities;
        }

        public void SaveChangesetResponse(Guid state, Microsoft.Synchronization.ClientServices.ChangeSetResponse response)
        {
            Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), state.ToString());

            ThrowIfDisposed();

            IEnumerable<PerstOfflineEntity> pendingChanges;
            if (!_pendingChanges.TryGetValue(state, out pendingChanges))
            {
                throw new ArgumentException("Unknown changeset id", "state");
            }

            _database.BeginTransaction();
            try
            {
                // apply received updates
                if (response.UpdatedItems != null && response.UpdatedItems.Count > 0)
                {
                    // process the server's response to inserts from client side
                    foreach (IOfflineEntity item in response.UpdatedItems)
                    {
                        // 4.d.a.a. "Save any changes received in the response. These changes contains acknowledgement from server for client inserts or errors and conflicts. For inserts, save the EntityId, EntityEtag and EntityEditUri metadata values to the local store."
                        // http://msdn.microsoft.com/en-us/library/gg299005%28v=SQL.110%29.aspx

                        /*
                         * The Updated items may fall under two categories
                            1. Just the entity with no Error or Conflict values attached. This normally refers to the successful ack of a client side insert on the server. In this case you will have to sotre the metadata values the server returned. But it might be possible that the server might have changed some values of certain columns due to some business logic on server.

                            2. Entity which have Error and Conflicts - This is the other one. There is one caveat though. If the client sent an insert and it had a conflict or error on the server then the winning/current value of the entity on server will be in the live one. So for an client insert that had a insert-insert conflict and user took a resolution of server wins the columns values might have changed.

                            You will have to hold some reference to indicate that the losing entity in the updated item is the actual insert as opposed to the live value. However if you think logically if the client applies all values from the "Live" version then its technically in sync with the server. The Wm6.5 client doesnt persist errors or conflicts and hence it always takes and updates all non pk columns from the list. Hope this helps.
                         *
                         * http://social.msdn.microsoft.com/Forums/en-US/synclab/thread/0a0b059c-6b35-4041-8f3a-70a03a645e5e
                         */

                        PerstOfflineEntity incomingEntity = (PerstOfflineEntity)item;

                        ApplyIncomingEntity(incomingEntity);
                    }
                } // response.UpdatedItems

                if (response.Conflicts != null && response.Conflicts.Count > 0)
                {
                    Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), "Sync conflicts");

                    foreach (var conflict in response.Conflicts)
                    {
                        // We have an conflict so apply the LiveEntity
                        PerstOfflineEntity liveEntity = (PerstOfflineEntity)conflict.LiveEntity;

                        // For a SyncError, which resulted from a client insert, the winning item may be a tombstone version
                        // of the client entity. In this case, the ServiceMetadata.Id property of the LiveEntity will be null.
                        // We need to lookup the item using primary keys in order to update it.
                        if (conflict is SyncError)
                        {
                            if (!liveEntity.ServiceMetadata.IsTombstone)
                            {
                                // If the live entity is not a tombstone, then we just need to update the entity.
                                ApplyIncomingEntity(liveEntity);
                            }
                            else
                            {
                                // At this point, the LiveEntity is a tombstone and does not have primary key info.

                                // If the live entity is a tombstone, then delete the item by looking up the primary key
                                // from the error entity.
                                // The error entity in this case will have both Id and the primary keys.
                                PerstOfflineEntity errorEntity = (PerstOfflineEntity)((SyncError)conflict).ErrorEntity;
                                errorEntity.ServiceMetadata.IsTombstone = true;
                                errorEntity.ServiceMetadata.Id = null;

                                // insert has been rejected on the server side
                                DeleteByIncomingEntity(errorEntity);
                            }
                        }
                        else
                        {
                            ApplyIncomingEntity(liveEntity);
                        }
                    }

                }

                // walk tracked changes sent to server
                foreach (PerstOfflineEntity pendingChange in pendingChanges)
                {
                    if (pendingChange.ServiceMetadata.IsTombstone)
                    {
                        // delete tombstoned entities (there was no other response from the server so the delete has been accepted)
                        DeleteEntity(pendingChange);
                    }
                    else if (pendingChange.IsDirty)
                    {
                        // remove IsDirty flag
                        pendingChange.IsDirty = false;
                    }
                }

                // update OfflineContextMetadata record
                ContextMetadata metadata = _database.GetRecords<ContextMetadata>(true).First();
                metadata.Anchor = response.ServerBlob;
                metadata.Modify();

                _database.CommitTransaction();

                // remove pending changes reference
                _pendingChanges.Remove(state);
            }
            catch (Exception e)
            {
                Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("{0}: {1}", e.GetType().Name, e.Message));
                _database.RollbackTransaction();
            }
        }

        /// <summary>
        /// Save changes retrieved from the sync service. This method is called to save changes from a download response.
        /// </summary>
        /// <param name="serverBlob">New blob received from the service.</param>
        /// <param name="entities">List of entities received from the service.</param>
        public void SaveChangeset(byte[] anchor, IEnumerable<PerstOfflineEntity> downloadedEntities)
        {
            Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), downloadedEntities.Count().ToString());

            ThrowIfDisposed();

            _database.BeginTransaction();
            try
            {
                foreach (PerstOfflineEntity entity in downloadedEntities)
                {
                    if (entity.ServiceMetadata.IsTombstone)
                    {
                        DeleteByIncomingEntity(entity);
                    }
                    else
                    {
                        AddOrUpdateEntity(entity);
                    }
                }

                // update OfflineContextMetadata record
                ContextMetadata metadata = _database.GetRecords<ContextMetadata>(true).First();
                metadata.Anchor = anchor;
                metadata.Modify();

                _database.CommitTransaction();
            }
            catch (Exception e)
            {
                Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("{0}: {1}", e.GetType().Name, e.Message));
                _database.RollbackTransaction();
            }
        }

        public void ForgetChangeset(Guid state)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod());

            ThrowIfDisposed();

            if (_pendingChanges != null)
            {
                _pendingChanges.Remove(state);
            }
        }

        [Obsolete("use Wipe() instead")]
        public void Clear()
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod());

            ThrowIfDisposed();

            _database.BeginTransaction();
            try
            {
                foreach (Type entityType in _schema.Collections)
                {
                    DropTable(entityType);
                }

                // reset OfflineContextMetadata record
                ContextMetadata metadata = _database.GetRecords<ContextMetadata>(true).First();
                metadata.Anchor = new byte[0];
                metadata.Modify();

                _database.CommitTransaction();
            }
            catch (Exception e)
            {
                Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), string.Format("{0}: {1}", e.GetType().Name, e.Message));
                _database.RollbackTransaction();
                throw new DatabaseException("Failed to clear", e);
            }
        }

        public void Wipe()
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod());

            ThrowIfDisposed();

            lock (_database)
            {
                if (_database.Storage.IsOpened())
                {
                    _database.Storage.Close();
                }

                using (IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    if (iso.DirectoryExists(_cachePath))
                    {
                        // prepare storage file name
                        string storageFileName = Path.Combine(_cachePath, STORAGE_FILENAME);

                        iso.DeleteFile(storageFileName);
                    }
                }

                _database = InitializeDatabase(_schema, _cachePath, _pagePoolSize, _cipherKey);

                // create tables and indices
                CreateTablesAndIndicies();
            }
        }

        public bool Backup(string backupFileName)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod());
            ThrowIfDisposed();

            bool result = false;
            lock (_database)
            {
                _database.Storage.Commit();

                _database.Storage.Backup(backupFileName, _cipherKey);
                result = true;
            }
            return result;
        }

        public bool Restore(string backupFileName)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod());
            ThrowIfDisposed();

            bool result = false;

            lock (_database)
            {
                string storageFileName = Path.Combine(_cachePath, STORAGE_FILENAME);
                string tempFileName = string.Format("{0}1", storageFileName);

                // close storage
                if (_database.Storage.IsOpened())
                {
                    _database.Storage.Close();
                }

                using (IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    // create temp copy of current storage file
                    if (CopyFile(iso, storageFileName, tempFileName))
                    {
                        // try to copy given backup file over storage file
                        if (CopyFile(iso, backupFileName, storageFileName))
                        {
                            // success
                            result = true;
                        }
                        else
                        {
                            // restore temp copy of old storage file
                            CopyFile(iso, tempFileName, storageFileName);
                        }

                        // delete temp copy of old storage file
                        iso.DeleteFile(tempFileName);
                    }
                }

                // re-open storage
                if (!string.IsNullOrEmpty(_cipherKey))
                {
                    _database.Storage.Open(storageFileName, _pagePoolSize, _cipherKey);
                }
                else
                {
                    _database.Storage.Open(storageFileName, _pagePoolSize);
                }

                // create tables and indices (if required)
                CreateTablesAndIndicies();
            }

            return result;
        }

        public void Shrink()
        {
            string tempFileName = Path.Combine(_cachePath, string.Format("{0}.tmp", STORAGE_FILENAME));

            if (Backup(tempFileName))
            {
                Restore(tempFileName);
            }

            using (IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
            {
                iso.DeleteFile(tempFileName);
            }
        }

        public void ExportXml(string exportFileName)
        {
            //Logger.Log(System.Reflection.MethodBase.GetCurrentMethod());
            ThrowIfDisposed();

            lock (_database)
            {
                _database.Storage.Commit();

                using (IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(exportFileName, FileMode.Create, FileAccess.Write, iso))
                    {
                        StreamWriter writer = new StreamWriter(stream);
                        _database.Storage.ExportXML(writer);
                    }
                }
            }

        }

        #endregion

    }
}
